package bsh.meta;

import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;

import bsh.CompiledScript;
import bsh.LocalVariable;
import bsh.ReflectionUtils;
import bsh.TypeResolver;
import bsh.asm.acessor.LocalObjectAccessor;
import bsh.asm.acessor.ObjectAccessor;
import bsh.compilation.CompilationOptions;
import bsh.compilation.DebugOptions;
import bsh.exception.CompilationAssertionFailed;
import bsh.impl.CachedTypeResolver;

/**
 * Escopo generico
 * 
 * @author takeshi
 */
public class ScopeImpl implements Scope {

    private final Scope parent;

    private final Map<String, LocalVariable> variables = new HashMap<String, LocalVariable>();

    private final Map<String, ObjectAccessor> accessors = new HashMap<String, ObjectAccessor>();

    private final ScopeType type;

    private int offset;

    private final Label startLabel = new Label(), endLabel = new Label();

    private boolean startVisited, endVisited;

    private final TypeResolver typeResolver;

    /**
     * C'tor. Cria um escopo global com offset de variaveis locais comecando por 0
     */
    public ScopeImpl() {
        this(0, null, ScopeType.GLOBAL);
    }

    /**
     * C'tor. Cria um escopo filho do escopo passado, com offset pelo menos 1
     * 
     * @param parent {@link ScopeImpl}
     * @param type {@link ScopeType}
     */
    public ScopeImpl(ScopeImpl parent, ScopeType type) {
        this(Math.max(parent.offset, 1), parent, type);
    }

    /**
     * C'tor
     * 
     * @param offset int offset de variaveis locais
     * @param parent o parent scope
     * @param type {@link ScopeType}
     */
    private ScopeImpl(int offset, Scope parent, ScopeType type) {
        super();
        this.type = type;
        this.offset = offset;
        this.parent = parent;
        if (parent == null) {
            this.typeResolver = new CachedTypeResolver();
        } else {
            this.typeResolver = new CachedTypeResolver(parent);
        }
    }

    /**
     * @return boolean
     */
    public boolean isRootScope() {
        return this.parent == null;
    }

    /**
     * {@inheritDoc}
     * 
     * @see bsh.meta.Scope#getVariable(java.lang.String)
     */
    public LocalVariable getVariable(String name) {
        final LocalVariable lv = this.variables.get(name);
        if (lv == null) {
            return localVariableNotFound(name);
        }
        return lv;
    }

    /**
     * Callback method for when a localVariable canno be found on this scope. It defaults to searching in the parent scope if this is not
     * the top level scope
     * 
     * @param name the variable name
     * @return {@link LocalVariable} or <code>null</code> if available
     */
    protected LocalVariable localVariableNotFound(String name) {
        if (this.parent != null) {
            return this.parent.getVariable(name);
        }
        return null;
    }

    /**
     * {@inheritDoc}
     */
    public void addAccessor(String name, ObjectAccessor accessor) {
        this.accessors.put(name, accessor);
    }

    /**
     * {@inheritDoc}
     */
    public ObjectAccessor getAccessor(String name) {
        ObjectAccessor accessor = this.accessors.get(name);
        if (accessor != null) {
            return accessor;
        }
        return accessorNotFound(name);
    }

    /**
     * Callback for when an {@link ObjectAccessor} cannot be found. It defaults to calling {@link #getAccessor(String)} in the parent scope
     * if this is not the top level scope
     * 
     * @param name the name of the {@link ObjectAccessor}
     * @return {@link ObjectAccessor} or null if none can be found
     */
    protected ObjectAccessor accessorNotFound(String name) {
        if (this.parent != null) {
            return this.parent.getAccessor(name);
        }
        return null;
    }

    /**
     * {@inheritDoc}
     * 
     * @see bsh.meta.Scope#addLocalVariable(java.lang.String, org.objectweb.asm.Label, bsh.meta.TypeWrapper)
     */
    public LocalVariable addLocalVariable(String name, Label start, TypeWrapper variableType) {
        if (this.isRootScope()) {
            throw new CompilationAssertionFailed("cannot add local variable to root scope");
        }
        if (name != null) {
            if (this.variables.get(name) != null) {
                throw new CompilationAssertionFailed("variable " + name + " already defined on scope");
            }
        }
        LocalVariable lv = new LocalVariable(name, this.getOffset(), variableType, start, this.endLabel);
        this.increaseOffset(variableType.numberOfSlots());
        if (name != null) {
            this.variables.put(name, lv);
            this.accessors.put(name, new LocalObjectAccessor(lv));
            this.registerVariable(lv);
        }
        return lv;
    }

    /**
     * {@inheritDoc}
     */
    public LocalVariable addMethodParameter(String name, TypeWrapper variableType) {
        return this.addLocalVariable(name, this.startLabel, variableType);
    }

    /**
     * {@inheritDoc}
     * 
     * @see bsh.meta.Scope#registerVariable(bsh.LocalVariable)
     */
    public void registerVariable(LocalVariable lv) {
        if (this.parent == null) {
            return;
        }
        this.parent.registerVariable(lv);
    }

    /**
     * {@inheritDoc}
     * 
     * @see bsh.meta.Scope#getRootScope()
     */
    public ScopeImpl getRootScope() {
        if (this.parent == null) {
            return this;
        }
        return (ScopeImpl) this.parent.getRootScope();
    }

    /**
     * {@inheritDoc}
     */

    public Label getStartLabel() {
        return this.startLabel;
    }

    /**
     * {@inheritDoc}
     */
    public Label getEndLabel() {
        return this.endLabel;
    }

    /**
     * {@inheritDoc}
     */
    public void installStartLabel(MethodVisitor visitor, CompilationOptions options) {
        if (this.startVisited) {
            throw new CompilationAssertionFailed("trying to install start scope twice");
        }
        visitor.visitLabel(this.startLabel);
        this.startVisited = true;
        if (options.debugOptions().contains(DebugOptions.VIRTUAL_STACK_FRAMES) && !this.isInsideConstructor()) {
            // start new scope
            // loading 'this'
            visitor.visitVarInsn(Opcodes.ALOAD, 0);
            visitor.visitLdcInsn(this.toString());
            visitor.visitMethodInsn(Opcodes.INVOKEINTERFACE, Type.getInternalName(CompiledScript.class), "newStackFrame",
                    Type.getMethodDescriptor(Type.VOID_TYPE, new Type[]{ Type.getType(String.class) }));
        }
    }

    /**
     * {@inheritDoc}
     */
    public boolean isInsideConstructor() {
        MethodScope method = this.currentMethod();
        if (method == null) {
            return false;
        }
        return method.isInsideConstructor();
    }

    /**
     * {@inheritDoc}
     */
    public void installEndLabel(MethodVisitor visitor, CompilationOptions options) {
        if (this.endVisited) {
            throw new CompilationAssertionFailed("trying to install end scope twice");
        }
        visitor.visitLabel(this.endLabel);
        this.endVisited = true;
        if (options.debugOptions().contains(DebugOptions.VIRTUAL_STACK_FRAMES) && !this.isInsideConstructor()) {
            visitor.visitVarInsn(Opcodes.ALOAD, 0);
            visitor.visitMethodInsn(Opcodes.INVOKEINTERFACE, Type.getInternalName(CompiledScript.class), "popStack",
                    Type.getMethodDescriptor(Type.VOID_TYPE, new Type[]{}));
        }
    }

    /**
     * {@inheritDoc}
     */
    public ScopeType getType() {
        return this.type;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String toString() {
        return "[SCOPE: " + this.type + "]";
    }

    /**
     * {@inheritDoc}
     * 
     * @see bsh.meta.Scope#increaseOffset(int)
     */
    public void increaseOffset(int size) {
        if (this.isRootScope()) {
            throw new CompilationAssertionFailed("cannot increase local variable offset on root context");
        } else {
            this.offset += size;
        }
    }

    /**
     * {@inheritDoc}
     * 
     * @see bsh.meta.Scope#getOffset()
     */
    public final int getOffset() {
        if (this.isRootScope()) {
            throw new CompilationAssertionFailed("cannot increase local variable offset on root context");
        } else {
            return this.offset;
        }
    }

    /**
     * @return {@link Map}
     */
    public Map<String, LocalVariable> getVariables() {
        return variables;
    }

    /**
     * @return {@link Map}
     */
    public Map<String, ObjectAccessor> getAccessors() {
        return accessors;
    }

    /**
     * {@inheritDoc}
     */
    public Map<String, ObjectAccessor> allVisibleAccessors() {
        Map<String, ObjectAccessor> all;
        if (parent != null) {
            all = parent.allVisibleAccessors();
        } else {
            all = new HashMap<String, ObjectAccessor>(this.accessors.size());
        }
        all.putAll(this.accessors);
        return all;
    }

    /**
     * {@inheritDoc}
     */
    public TypeWrapper resolveType(String name) {
        TypeWrapper resolveType = typeResolver.resolveType(name);
        if (resolveType == null && this.parent != null) {
            return this.parent.resolveType(name);
        }
        return resolveType;
    }

    /**
     * {@inheritDoc}
     */
    public void addTypeAlias(String name, TypeWrapper typeWrapper) {
        typeResolver.addTypeAlias(name, typeWrapper);
    }

    /**
     * {@inheritDoc}
     */
    public void addWildcardImport(String packageName) {
        typeResolver.addWildcardImport(packageName);
    }

    /**
     * {@inheritDoc}
     */
    public ReflectionUtils getReflectionUtils() {
        return typeResolver.getReflectionUtils();
    }

    /**
     * {@inheritDoc}
     */
    public Map<String, TypeWrapper> getImports() {
        return typeResolver.getImports();
    }

    /**
     * {@inheritDoc}
     */
    public Set<String> getWildCards() {
        return typeResolver.getWildCards();
    }

    /**
     * {@inheritDoc}
     */
    public MethodScope currentMethod() {
        if (this instanceof MethodScope) {
            return (MethodScope) this;
        } else if (this.parent != null) {
            return this.parent.currentMethod();
        } else {
            return null;
        }
    }

    /**
     * {@inheritDoc}
     */
    public boolean isStaticScope() {
        if (currentMethod() == null) {
            return true;
        }
        return currentMethod().getMethod().isStatic();
    }

    /**
     * {@inheritDoc}
     */
    public void copyTypeAliases(TypeResolver otherTypeResolver) {
        this.typeResolver.copyTypeAliases(otherTypeResolver);
    }

}
